Skip to content

Fix kernel async statement telemetry handle - #923

Open
jay-xiao446 wants to merge 10 commits into
mainfrom
jay/kernel-async-telemetry-handle
Open

Fix kernel async statement telemetry handle#923
jay-xiao446 wants to merge 10 commits into
mainfrom
jay/kernel-async-telemetry-handle

Conversation

@jay-xiao446

@jay-xiao446 jay-xiao446 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Use the retained owning kernel async handle for the first full in-process async flow: status polling via get_query_state and the first result fetch via get_execution_result.
  • Mark result streaming once the owning handle is claimed for fetch, then fall back to attach-by-ID for later state/result calls.
  • Preserve attach-by-ID for missing-owner cases such as re-fetch, fresh cursor, or cross-process lookup.
  • Update unit coverage and lifecycle comments for the owning-handle versus attach-by-ID behavior.

Testing

  • .venv/bin/python -m pytest tests/unit/test_kernel_client.py -q

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — the owning-handle-first / attach-by-id-fallback change is correct: the claim is check-and-set under _async_handles_lock, cleanup is consistent across close_command/close_session, and the new unit + e2e tests cover the main paths. One low-severity note on an unclear telemetry-finalization edge when result-set construction fails after a successful await_result().

stream = handle.await_result()
except Exception as exc:
if uses_owning_handle:
with self._async_handles_lock:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The owning-handle failure path only discards _async_result_stream_started when await_result() raises. If await_result() succeeds (marker stays set) but the subsequent KernelResultSet.__init__arrow_schema() raises and is re-wrapped, the guid remains marked as started. A later retry then takes the attach-by-id (no-op telemetry) branch.

Whether this loses the ExecuteStatementAsync telemetry row depends on when the kernel finalizes it: if finalization happens when await_result() returns, this is harmless (telemetry already committed). If finalization only completes once the result stream is drained, the telemetry is lost on this retry because the owning handle is never reused. The PR's own comments ("first in-process result stream", "clear the claimed marker so a retry can still use the telemetry-bearing owning handle") are ambiguous on this point, and the added test_get_execution_result_owning_handle_failure_can_retry_owning_handle only exercises the await_result()-raises case, not the construct-failure-after-await case. Worth confirming the finalization semantics and, if drain-based, discarding the marker on the construction-failure path too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.

No safe code change is actionable here, and the deciding fact needs kernel-team judgment. The reviewer's suggested fix — discard _async_result_stream_started on the construction-failure path (await succeeds, then _make_result_set/arrow_schema() raises) — would be unsafe: the owning handle's await_result() is single-shot by design (confirmed by test_get_execution_result_is_re_callable, which deliberately re-attaches by id for the second fetch instead of re-calling owning_handle.await_result()). Discarding the marker would route a retry back through the owning handle and re-invoke await_result() on an already-consumed stream — the exact double-consume the marker prevents. Moreover, once await_result() has returned, the stream is materialized and single-shot, so attach-by-id is the only correct retry path regardless. Whether the rare construct-failure-after-await case actually loses the ExecuteStatementAsync telemetry row depends on kernel-internal finalization timing (finalize-at-return vs finalize-at-drain), which has no observable surface in databricks-sql-python and cannot be verified from this repo or a unit test here. The module's own comments assume return-based finalization (making the concern moot); if drain-based, there is no safe in-process remedy. Flagging for a human to confirm finalization semantics with the kernel team.

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Solid, well-tested fix. The owning-handle-first / attach-by-id-fallback logic is correctly lock-guarded, the claimed marker is reset on await failure to preserve retry-with-owning-handle, and unit + e2e coverage and lifecycle comments are updated to match. One low-severity edge: the result-set-construction failure path (second except) doesn't reset _async_result_stream_started, so a retry after that failure loses the telemetry-bearing owning handle — see inline note. The is_telemetry_enabled short-circuit for use_kernel (overriding even force_enable_telemetry) is intentional and tested.

Comment thread src/databricks/sql/backend/kernel/client.py

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — the async owning-handle ownership model (owning handle for first in-process status/result, attach-by-id for re-fetch/cross-process, marker cleared on close/session-close) is coherent and well covered by the new unit tests, and the use_kernel telemetry short-circuit is intentional (verified against the added test). One Low note about asymmetric marker cleanup when result-set construction fails after a successful await_result().

Comment thread src/databricks/sql/backend/kernel/client.py

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues identified by the review bot.

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium

Looks solid overall — the owning-handle-first / attach-by-id fallback logic, locking, and lifecycle cleanup (close_command / close_session) are consistent and well-tested. One medium concern: the result-set-construction failure path in get_execution_result doesn't clear the _async_result_stream_started marker the way the sibling await_result failure path does, so a retry after a construction error would silently drop to attach-by-id and lose the async telemetry this PR is meant to preserve.

Comment thread src/databricks/sql/backend/kernel/client.py

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — the owning-handle-first / attach-by-id-fallback logic is correct, lock usage is sound, and the marker lifecycle (claim on fetch, discard on await failure, retain on post-await construction failure, clear on close) is consistent across get_query_state, get_execution_result, close_command, and close_session. One low-severity coverage gap: the deliberately-documented "marker retained when result-set construction fails after a successful await" branch isn't locked by a unit test, unlike its sibling await-failure branch.

Comment thread src/databricks/sql/backend/kernel/client.py
Addresses:
  - #3836884412 at src/databricks/sql/backend/kernel/client.py:816

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Solid, well-tested fix — the owning-handle-first / attach-by-id-fallback logic and the _async_result_stream_started marker lifecycle (add on claim, discard on await_result() failure to allow an owning-handle retry, deliberately retain on result-set construction failure to route retries through a fresh attach-by-id stream) are internally consistent and match the added unit tests. One low-severity note inline about the new concurrency surface. Separately (not inline, since it's outside the diff): test_async_result_resumable_from_a_fresh_cursor's docstring claims it proves "state/results come from the server (attach-by-id), not connector-side per-cursor tracking," but because _async_handles is shared per-connection, that same-connection resume now goes through the retained owning handle rather than attach-by-id — the test still passes but no longer exercises the path its docstring describes; consider updating it or resuming from a second connection.

# concurrent polling of one async id is unsupported — the supported cross-cursor
# resume path re-attaches by id (the attach-by-id fallback below) once result
# streaming has been claimed.
with self._async_handles_lock:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — This change trades the previously concurrency-safe behavior (always attach-by-id, where every get_query_state / get_execution_result call created its own server-side handle) for use of a shared owning handle before result streaming is claimed. As the new concurrency note candidly documents, handle.status() (and handle.await_result()) now run outside the lock on the connection-shared owning handle. Note that _async_handles is keyed per-connection, not per-cursor, so a fresh cursor on the same connection that adopts the command id (the resume flow exercised by tests/e2e/test_kernel_backend.py::test_async_result_resumable_from_a_fresh_cursor) will also resolve the owning handle and can race the original submitter's polling on the same underlying kernel handle — not just a hand-rolled two-cursor poll. This is an intentional, documented limitation and the common single-submitter flow is safe, so flagging as low: worth confirming the kernel ExecutedAsyncStatement either tolerates concurrent status() or that same-connection concurrent resume is genuinely out of scope.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I corrected the misleading documentation the reviewer flagged. The docstring for get_query_state now accurately reflects that the owning-handle path is per-connection — any cursor on the submitting connection (including a fresh cursor resuming the id) resolves the same owning handle until result streaming is claimed — consistent with the inline concurrency note. Tests pass (125 in the kernel client file).

Pushed 23b4330.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pushed commit only rewords the docstring — the owning-handle-under-lock behavior at line 721 is unchanged, and the substantive question I raised (does the kernel ExecutedAsyncStatement actually tolerate concurrent status(), or is same-connection concurrent resume genuinely out of scope?) is neither demonstrated nor answered by clearer prose. Better documentation of a limitation isn't verification that the limitation is safe, so I'm keeping this open at Low for a human to confirm the underlying kernel-handle behavior; no further action is needed if that concurrency is confirmed acceptable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.

Converged to a needs-human question that no code change here can settle: whether the kernel wheel's ExecutedAsyncStatement.status() is safe under concurrent calls on the shared per-connection owning handle. A human must confirm one of: (a) the kernel handle's status()/await_result() tolerate concurrent invocation, or (b) same-connection concurrent resume (tests/e2e/test_kernel_backend.py::test_async_result_resumable_from_a_fresh_cursor racing the original submitter's polling) is genuinely out of scope. Verifying this requires reading the databricks-sql-kernel wheel internals or running the e2e resume test against a live warehouse — this follow-up job has no live-warehouse credentials, so it can't run here. Rewording the docstring again was already rejected by the reviewer as non-verification, and dropping the shared owning handle would revert this PR's intended async-telemetry-finalization fix, so neither is an appropriate change. Flagging for human confirmation of kernel-handle concurrency behavior.

Addresses:
  - #3836901963 at src/databricks/sql/backend/kernel/client.py:716

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — the owning-handle-first async telemetry fix is carefully implemented and well-covered by unit tests (claim/discard bookkeeping is consistent, and the set is cleaned up in close_command/close_session). One low-severity note: the change shares a single kernel handle for pre-stream status polling and calls status() outside the lock, narrowing a previously-safe concurrent-polling path to "documented-but-unenforced."

Comment thread src/databricks/sql/backend/kernel/client.py
Addresses:
  - #3836923314 at src/databricks/sql/backend/kernel/client.py:729

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium

Looks solid overall — the per-method reservation/claim bookkeeping is correct and well-tested. One medium concurrency concern: get_execution_result doesn't consult _async_status_in_flight, so a concurrent status-poll and result-fetch can still operate on the same shared owning handle — the exact cross-thread use the PR's own comment declares unsafe. Narrow in the normal serial poll-then-fetch flow, but worth guarding or documenting.

Comment thread src/databricks/sql/backend/kernel/client.py Outdated
Addresses:
  - #3836943236 at src/databricks/sql/backend/kernel/client.py:823

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues identified by the review bot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engineer-bot Maintainer-applied gate: triggers engineer-bot (bug-fix on issue / take-over on PR).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants